#include <QtScript>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QMap>
#include <QResource>
#include <QUiLoader>
#include <QDebug>

#include "QyScript.h"

#define QY_SCRIPT_SCRIPTOBJECTNAME "script"

/**************************************************************************************************/

QyScript::QyScript( QObject * parent ) : QObject(parent) {

	p_engine = new QScriptEngine(this);

	addGlobalObject( this, QY_SCRIPT_SCRIPTOBJECTNAME );

}

QyScript::~QyScript() {
}

//! выполняет указанный файл. Перед выполнением, генерируется "словарь" systemEnvironment. После выполнения файла, происходит сбор "мусора".
bool QyScript::evaluateFile( const QString & fileName ) {

	evaluatedFiles.clear();

    QMap<QString,QVariant> envMap;
    QStringList envList = QProcess::systemEnvironment();

    foreach ( QString entry, envList ) {

		QStringList keyVal = entry.split('=');

		if ( keyVal.isEmpty() )
			continue;

		QString name( keyVal.takeFirst() );

		envMap[name] = keyVal.join("=");

    }

	QScriptValue globalObject = p_engine->globalObject();

    globalObject.setProperty( "systemEnvironment", p_engine->toScriptValue(envMap) );

	bool result = internalEvaluateFile(fileName);

	evaluatedFiles.clear();
	p_engine->collectGarbage();

	return result;
}

//! Если имя объекта не указано, то функция попытается извлечь имя из свойства objectName. Если и оно не задано, то объект зерегестрирован не будет.
bool QyScript::addGlobalObject( QObject * object, QString name ) {

	if ( name.isEmpty() ) {

		name = object->objectName();

		if ( name.isEmpty() ) {

			qWarning( "Can't register object unnamed object." );

			return false;
		}

	}

	QScriptValue globalObject = p_engine->globalObject();

	globalObject.setProperty( name, p_engine->newQObject(object) );

	return true;
}

//! Дает возможность вставки файлов друг вдруга, на подобии одноименной директивы в C/C++, вставка происходит на этапе выполнения, по сути дела, include является функцией, которая "выполняет" указанный файл. В случае удачного выполнения возвращает true. Если вызвать include НЕ во время выполнения скрипта, то ничего не произойдет, метод просто вернет false.
bool QyScript::include( const QString & fileName ) {

	// qDebug() << __PRETTY_FUNCTION__ << p_engine->isEvaluating();

	if ( !p_engine->isEvaluating() ) {

		// qDebug() << __PRETTY_FUNCTION__ << "not evaluating";

		return false;
	}

	// qDebug() << __PRETTY_FUNCTION__ << fileName;

    QString includeFile = p_engine->globalObject().property(QY_SCRIPT_SCRIPTOBJECTNAME).
		property("currentPath").toString() + "/" + fileName;

	QScriptContext * context = p_engine->currentContext();

    if ( !QFile::exists(includeFile) ) {

		// qWarning( "File %s not found.", qPrintable(includeFile) );
        context->throwError( QString("File %1 not found.").arg(includeFile) );

		return false;
	}

    if ( !internalEvaluateFile( includeFile ) ) {

		// qWarning( "Can't evaluate %s.", qPrintable(includeFile) );
		context->throwError( QString("Can't evaluate %1.").arg(includeFile) );

		return false;
	}

    return true;
}

//! Регистрирует ресурс.
bool QyScript::addResource( const QString & fileName ) {

	return QResource::registerResource(fileName);
}

//! Если расширение уже задействовано или не найдено, то ничего не произойдет, в первом случае вернет true во втором false.
bool QyScript::importExtension( const QString & name ) {

	if ( name.isEmpty() ) {

		qDebug() << "Extension name is empty. Extension not imported";

		return false;
	}

	if ( p_engine->importedExtensions().contains(name) )
		return true;

	if ( !p_engine->availableExtensions().contains(name) ) {

		qWarning( "Extension \"%s\" is not avilable. Available names: \n  %s", qPrintable(name),
			qPrintable( p_engine->availableExtensions().join("\n  ") ) );

		return false;
	}

	return p_engine->importExtension(name).isUndefined() && checkErrors();
}

QStringList QyScript::availableExtensions() const {

	return p_engine->availableExtensions();
}

QStringList QyScript::importedExtensions() const {

	return p_engine->importedExtensions();
}

QWidget * QyScript::loadUiFile( const QString & fileName, QWidget * parent ) {

	QFile file(fileName);

	if ( !file.open(QIODevice::ReadOnly) )
		return NULL;

	return QUiLoader().load( &file, parent );
}

QString QyScript::readAllFromFile( const QString & fileName ) const {

	QFile file(fileName);

	if ( file.open(QFile::ReadOnly) )
		return file.readAll();

	return QString();
}

void QyScript::generatePropertyMap( const QScriptValue & object, const QString & name,
	QMap<QString,QVariant> & propertyMap )
{

	QScriptValueIterator it(object);

	while ( it.hasNext() ) {

		it.next();

		QScriptValue value( it.value() );
		QString valueName( it.name() );

		if ( value.isFunction() )
			continue;

		if ( name.count() )
			valueName = name + "." + valueName;

		if ( value.isObject() )
			generatePropertyMap( value, valueName, propertyMap );
		else
			propertyMap[valueName] = value.toVariant();

	}

}

bool QyScript::internalEvaluateFile( const QString & fileName ) {

	QFileInfo info(fileName);
	QString absoluteFilePath = info.absoluteFilePath();
	QString absolutePath = info.absolutePath();

    if ( evaluatedFiles.contains(absoluteFilePath) )
        return true;

	evaluatedFiles.append(absoluteFilePath);

    QFile file(absoluteFilePath);

    if ( !file.open(QFile::ReadOnly) ) {

		qWarning( "Can't open file \"%s\"", qPrintable(absoluteFilePath) );

		return false;
	}

	QScriptValue scriptObject = p_engine->globalObject().property(QY_SCRIPT_SCRIPTOBJECTNAME);
	QScriptValue lastPath = scriptObject.property("currentPath"),
		lastFilePath = scriptObject.property("currentFilePath");

	scriptObject.setProperty( "currentPath", p_engine->toScriptValue(absolutePath) );
	scriptObject.setProperty( "currentFilePath", p_engine->toScriptValue(absoluteFilePath) );

	// qDebug() << "evaluate file" << absoluteFilePath;

	QScriptValue result = p_engine->evaluate( file.readAll(), absoluteFilePath );

	scriptObject.setProperty( "lastResult", result );

	if ( !checkErrors() )
		return false;

	scriptObject.setProperty( "currentPath", lastPath );
	scriptObject.setProperty( "currentFilePath", lastFilePath );

	// QMap<QString,QVariant> propertyMap;
	// QScriptValue globalObject = p_engine->globalObject();

	// generatePropertyMap( result, "", propertyMap );

	// QMapIterator<QString,QVariant> propIter(propertyMap);
	// while ( propIter.hasNext() ) {

		// propIter.next();

		// globalObject.setProperty( propIter.key(), p_engine->toScriptValue( propIter.value() ) );

		// qDebug() << "add go prop" << propIter.key();

	// }

	return true;
}

bool QyScript::checkErrors() {

	if ( p_engine->hasUncaughtException() ) {

		QString errorText( p_engine->uncaughtException().toString() ),
			fileName( p_engine->globalObject().property("currentFilePath").toString() );
		QStringList backtrace( p_engine->uncaughtExceptionBacktrace() );
		int line = p_engine->uncaughtExceptionLineNumber();

		if ( fileName.isEmpty() && evaluatedFiles.count() )
			fileName = evaluatedFiles.last();

		qDebug() << qPrintable(errorText) << "in" << fileName << "on" << line << "\nBacktrace:\n  " << backtrace.join("\n  ");

		emit scriptError( errorText, fileName, line, backtrace );

		p_engine->clearExceptions();

		return false;
	}

	return true;
}
